// ==UserScript==
// @name        Ci Helper
// @namespace   Violentmonkey Scripts
// @match       https://gitee.com/openharmony/*/pulls/*
// @version     1.0.6
// @author      milkpotatoes
// @require     ../utils/utils.js
// @homepageURL https://gitee.com/milkpotatoes/tools-for-openharmony-repo
// @description Button defines for ci helper
// ==/UserScript==

'use strict';

const JUMP_WHITELIST_KEY = 'CI_HELPER_JUMP_WHITELIST';
const JUMP_BLACKLIST_KEY = 'CI_HELPER_JUMP_BLACKLIST';
const OPEN_WHITELIST_KEY = 'CI_HELPER_OPEN_WHITELIST';
const OPEN_BLACKLIST_KEY = 'CI_HELPER_OPEN_BLACKLIST';

const PERMISSION_ALLOW_TEXT_RESOURCE = Object.freeze({
    default: '@en',
    'zh-CN': '同意',
    'en': 'Allow'
});
const PERMISSION_DENY_TEXT_RESOURCE = Object.freeze({
    default: '@en',
    'zh-CN': '拒绝',
    'en': 'Deny'
});

const getPermissionList = async (key, id) => {
    const lists = JSON.parse(await GM.getValue(key, '{}'));
    const list = lists[id] ?? [];
    const set = new Set(list);
    return set;
}

/**
 * 
 * @param {string} id 
 * @param {string} key 
 * @param {Set<string>} list 
 */
const savePermissionList = async (id, key, list) => {
    let lists;
    try {
        lists = JSON.parse(await GM.getValue(key, '{}'));
    } catch {
        lists = {};
    }
    lists[id] = [...list]
    GM.setValue(key, JSON.stringify(lists));
}

const confirmToNavigate = async (url, id, name, type) => {
    let wKey = '';
    let bKey = '';
    if (type === 'jump') {
        wKey = JUMP_WHITELIST_KEY;
        bKey = JUMP_BLACKLIST_KEY;
    } else if (type === 'open') {
        wKey = OPEN_WHITELIST_KEY;
        bKey = OPEN_BLACKLIST_KEY;
    } else {
        throw new TypeError('invalid type, type must be "open" or "jump".');
    }

    let oUrl = null;
    try {
        oUrl = new URL(url);
    } catch {
        oUrl = new URL(url, location.href);
    }
    if (oUrl.origin === location.origin) {
        return true;
    }
    const blackList = await getPermissionList(bKey, id);
    if (blackList.has(oUrl.origin)) return false;

    const whiteList = await getPermissionList(wKey, id);
    if (whiteList.has(oUrl.origin)) return true;
    const displayName = Utils.escapeHtml(name);
    const message = Utils.getI18NRes({
        default: '@en',
        'zh-CN': `是否允许 "${displayName}" ${type === 'open' ? '打开' : '前往'}以下地址?`,
        'en': `Allow "${displayName}" to ${type === 'jump' ? 'navigate' : type} the following address?`,
    });
    const alwaysAgree = Utils.getI18NRes({
        default: '@en',
        'zh-CN': '记住我的选择',
        'en': 'Don\'t ask again',
    });
    const displayUrl = `<span style="font-weight:bold">${oUrl.origin}</span>${oUrl.toString().substring(oUrl.origin.length)}`;
    const contentView = document.createElement('div');
    contentView.style.lineHeight = '2';

    contentView.innerHTML = `<span style="font-size:1.2em">${message}</span><br/>${displayUrl}
<label style="opacity:.8;user-select:none"><input type="checkbox"/>${alwaysAgree}</label>`;
    const chkBox = contentView.querySelector('input');
    return new Promise((r) => {
        const dialog = new AlertDialog()
            .setView(contentView)
            .addButton(Utils.getI18NRes(PERMISSION_DENY_TEXT_RESOURCE), {
                dismissOnClick: false,
                accent: type === 'open',
                events: {
                    click: () => {
                        if (chkBox.checked) {
                            blackList.add(oUrl.origin);
                            savePermissionList(id, bKey, blackList);
                        }
                        r(false);
                        dialog.dismiss();
                    }
                }
            })
            .addButton(Utils.getI18NRes(PERMISSION_ALLOW_TEXT_RESOURCE), {
                dismissOnClick: false,
                accent: type === 'jump',
                events: {
                    click: () => {
                        if (chkBox.checked) {
                            whiteList.add(oUrl.origin);
                            savePermissionList(id, wKey, whiteList);
                        }
                        r(true);
                        dialog.dismiss();
                    }
                }
            })
            .onDismiss(() => {
                r(false);
            });
        dialog.show();
    });
};

class HelperUtils {
    enableCanComment(e) {
        if (!Utils.prUtils.isLogin() || Utils.prUtils.getPRPage() !== Utils.PR_PAGE.COMMENTS ||
            Utils.prUtils.getPRState() == Utils.PR_STATE.CLOSED) {
            e.style.display = 'none';
            return false;
        }
        return true;
    }

    enableCanTriggerCI(e) {
        if (!this.enableCanComment(e) || Utils.prUtils.getPRAuthor() !== Utils.prUtils.getLoginUser()) {
            e.style.display = 'none';
        }
    }

    getCommentEditor() { throw new Error('HelperUtils not has current function'); }

    /**
     * 
     * @param {Event} e 
     * @param {string?} message 
     */
    triggerButtonComment(e, message) { throw new Error('HelperUtils not has current function'); }
    switchToComment(e) { throw new Error('HelperUtils not has current function'); }
    goToComment(e) { throw new Error('HelperUtils not has current function'); }
    openLoginDialog(e) { throw new Error('HelperUtils not has current function'); }
    /**
     * 
     * @param {Event} e 
     * @param {string?} url 
     */
    async openInNewTab(e, url) {
        const target = e.handleTarget ?? e.target;
        if (url === '' || url === undefined) {
            url = target.getAttribute('data');
        }
        if (await confirmToNavigate(url, target.getAttribute('id'), target.textContent, 'open')) {
            window.open(url);
        }
    }
    /**
     * 
     * @param {Event} e 
     * @param {string?} url 
     */
    async jumpTo(e, url) {
        const target = e.handleTarget ?? e.target;
        if (url === '' || url === undefined) {
            url = target.getAttribute('data');
        }
        if (await confirmToNavigate(url, target.getAttribute('id'), target.textContent, 'jump')) {
            location.assign(url)
        }
    }
}

let helperUtils;
const InitHelperUtils = (helper) => {
    helperUtils = helper;
}

const helpBtns = [
    {
        name: 'login',
        label: {
            'default': '@en',
            'zh-CN': '登录',
            'en': 'Login',
        },
        events: {
            create: (e) => {
                if (Utils.prUtils.isLogin()) {
                    e.style.display = 'none';
                }
            },
            click: e => helperUtils.openLoginDialog(e),
        },
    },
    {
        name: 'switch-to-comment',
        label: {
            'default': '@en',
            'zh-CN': '切换到评论',
            'en': 'Switch to comments',
        },
        events: {
            create: (e) => {
                if (!Utils.prUtils.isLogin() || Utils.prUtils.getPRPage() === Utils.PR_PAGE.COMMENTS) {
                    e.style.display = 'none';
                }
            },
            click: e => helperUtils.switchToComment(e),
        },
    },
    {
        name: 'disabled-note',
        label: {
            'default': '@en',
            'zh-CN': '以启用更多功能',
            'en': 'to active more tools',
        },
        style: ['note-style'],
        events: {
            create: (e) => {
                if (Utils.prUtils.isLogin() && Utils.prUtils.getPRPage() === Utils.PR_PAGE.COMMENTS) {
                    e.style.display = 'none';
                }
            }
        },
    },
    {
        name: 'trigger-oh-ci',
        label: {
            'default': '@en',
            'zh-CN': '触发OH门禁',
            'en': 'Trigger OH CI',
        },
        data: 'start build',
        style: ['green'],
        enable: ['*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'terminate-oh-ci',
        label: {
            'default': '@en',
            'zh-CN': '停止OH门禁',
            'en': 'Terminate OH CI',
        },
        data: 'stop build',
        style: ['red'],
        enable: ['*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'trigger-oh-static-check',
        label: {
            'default': '@en',
            'zh-CN': '触发静态检查',
            'en': 'Trigger code check',
        },
        data: 'static-check',
        enable: ['*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'trigger-ggwatcher-build',
        label: {
            'default': '@en',
            'zh-CN': '触发GGW门禁',
            'en': 'Trigger GGWatcher',
        },
        data: 'ggw build start',
        style: ['green'],
        enable: ['arkcompiler_*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'stop-ggwatcher-build',
        label: {
            'default': '@en',
            'zh-CN': '停止GGW门禁',
            'en': 'Stop GGWatcher',
        },
        data: 'ggw build stop',
        style: ['red'],
        enable: ['arkcompiler_*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'trigger-ggwatcher-review',
        label: {
            'default': '@en',
            'zh-CN': '启动GGW检视',
            'en': 'Start GGWatcher Review',
        },
        data: 'ggw review start',
        enable: ['arkcompiler_*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'resolve-ggwatcher-question',
        label: {
            'default': '@en',
            'zh-CN': 'GGW问题已解决',
            'en': 'Resolve GGWatcher Question',
        },
        data: 'ggw review resolve',
        enable: ['arkcompiler_*', 'page:COMMENTS', 'state:OPEN'],
        events: {
            create: e => helperUtils.enableCanTriggerCI(e),
            click: e => helperUtils.triggerButtonComment(e),
        },
    },
    {
        name: 'ci-disabled-note',
        label: {
            'default': '@en',
            'zh-CN': '仅打开的PR可触发流水线',
            'en': 'Only OPEN PR can trigger ci',
        },
        style: ['note-style'],
        enable: ['page:COMMENTS'],
        disable: ['state:OPEN'],
        events: {
            create: (e) => {
                if (!Utils.prUtils.isLogin()) {
                    e.style.display = 'none';
                }
            }
        },
    },
    {
        name: 'ci-disabled-notauthor',
        label: {
            'default': '@en',
            'zh-CN': '仅PR作者可触发流水线',
            'en': 'Only Author can trigger ci',
        },
        style: ['note-style'],
        enable: ['page:COMMENTS', 'state:OPEN'],
        events: {
            create: (e) => {
                if (!Utils.prUtils.isLogin() || Utils.prUtils.getPRAuthor() === Utils.prUtils.getLoginUser()) {
                    e.style.display = 'none';
                }
            }
        },
    },
    {
        name: 'blank',
        label: { 'default': '' },
        style: [
            'transparent',
            'css:flex: 1 0 auto',
            'css:pointer-events: none '
        ],
    },
    {
        name: 'locate-comment',
        label: { 'default': 'comment' },
        comment: {
            'default': '@en',
            'zh-CN': '跳转到评论框',
            'en': 'Locate comment editor',
        },
        style: [
            'material-icons',
            'transparent',
            'hover-effect',
        ],
        enable: ['page:COMMENTS'],
        events: {
            create: e => helperUtils.enableCanComment(e),
            click: e => helperUtils.goToComment(e),
        },
    },
    {
        name: 'scroll-to-bottom',
        label: { 'default': 'keyboard_arrow_down' },
        comment: {
            'default': '@en',
            'zh-CN': '单击滚动到底部\n双击回到顶部',
            'en': 'Click: S croll to bottom\nDbclick: Back to Top',
        },
        style: [
            'material-icons',
            'transparent',
            'hover-effect',
        ],
        events: {
            click: function () {
                clearTimeout(this.timer);
                this.timer = setTimeout(() => {
                    document.documentElement.scrollTo({
                        left: document.documentElement.scrollLeft,
                        top: document.body.scrollHeight - document.body.offsetHeight,
                        behavior: "smooth"
                    });
                }, 100);
            },
            dblclick: function () {
                clearTimeout(this.timer);
                document.documentElement.scrollTo({
                    left: document.documentElement.scrollLeft,
                    top: 0,
                    behavior: "smooth"
                });
            },
        },
    },
];

const PROCESS_ARG_OPTIONS = {
    repo: () => Utils.prUtils.getRepoInfo().repo,
    author: () => Utils.prUtils.getPRAuthor(),
    page: () => Utils.prUtils.getPRPage(),
    lang: () => Utils.prUtils.getWebLang(),
    state: () => Utils.prUtils.getPRState(),
    id: () => Utils.prUtils.getRepoInfo().id,
}
Object.freeze(PROCESS_ARG_OPTIONS);

/**
 * 
 * @param {SplitResult[]|SplitResult} args
 * @param {UserButtonEventHandler} handler
 * @returns {string[]|string}
 */
const processCommandArg = (args, handler) => {
    if (args instanceof SplitResult) {
        return args.processToken([
            ...Object.entries(PROCESS_ARG_OPTIONS),
            ...handler.constants,
            ...handler.variables,
            ...Object.entries(handler.extraData),
        ]);
    }
    return args.map(arg => arg.processToken([
        ...Object.entries(PROCESS_ARG_OPTIONS),
        ...handler.constants,
        ...handler.variables,
        ...Object.entries(handler.extraData),
    ]));
}

class SplitResult {
    markToken() {
        this.identifies.push([this.val.length, - 1]);
        this.unclosed.push(this.identifies.length - 1);
    }
    closeToken() {
        const idx = this.unclosed.pop();
        if (idx === undefined) return;
        this.identifies[idx][1] = this.val.length;
    }
    /**
     * @typedef { string|() => string} StoreValue 
     * @param {Record<string, StoreValue> | [string, StoreValue][] | Map<string, StoreValue>} store 
     * @returns {string}
     */
    processToken(store) {
        if (this.identifies.length == 0 || typeof store !== 'object' || store === null) {
            return this.val;
        }
        /**
         * @type {Map<string, StoreValue>}
         */
        const vals = (() => {
            if (store instanceof Map) return store;
            if (store instanceof Array) return new Map(store);
            return new Map(Object.entries(store))
        })();
        if (vals.size === 0) return this.val;

        let result = '';
        let last = 0;
        for (const [start, end] of this.identifies) {
            if (end === -1) {
                continue;
            }
            const token = this.val.substring(start + 1, end);
            const val = vals.get(token);
            if (val === undefined) {
                continue;
            }
            const to = typeof val === 'function' ? val() : String(vals.get(token));
            result += this.val.substring(last, start) + to;
            last = end + 1;
        }
        result += this.val.substring(last);
        return result;
    }
    val = '';
    /**
     * @type {[number,number][]}
     */
    identifies = [];
    unclosed = [];
}

class ControlNode {
    static #instance = new Map();
    static #contracting = false;
    name;
    static getInstance(name) {
        if (ControlNode.#instance.has(name)) {
            return ControlNode.#instance.get(name);
        }
        this.#contracting = true;
        const instance = new ControlNode(name);
        this.#contracting = false;
        this.#instance.set(name, instance);
        return instance;
    }
    constructor(name) {
        if (!ControlNode.#contracting) {
            throw new Error('Failed to construct, please use "getInstance" instead.');
        }
        this.name = name;
    }
    toString() {
        return this.name;
    }
}

class UserButtonEventHandler {
    static bTrue = ControlNode.getInstance('true');
    static bFalse = ControlNode.getInstance('false');
    static bBreak = ControlNode.getInstance('break');
    static bContinue = ControlNode.getInstance('continue');

    constructor(data, label, resources) {
        this.extraData.data = () => data;
        this.extraData.label = () => label;
        this.buttonResource = resources ?? null;
        this.extraData.returns = () => this.returns ? String(this.returns) : '';
    }
    buttonResource = null;
    /**
     * @type {Map<name, async EventListener[]>}
     */
    handlers = new Map();
    extraData = {};
    /**
     * @type {Map<string, string|number>}
     */
    constants = new Map();
    /**
     * @type {Map<string, string|number>}
     */
    variables = new Map();
    returns = undefined;
    target;
    timers = new Set();

    /**
     * 
     * @param {Event} ev 
     */
    async handleEvent(ev) {
        this.returns = undefined;
        /**
         * @type {Map<string, string|number>}
         */
        const cachedVariable = new Map(this.variables);
        const cachedConstant = new Map(this.constants);
        ev.handleTarget = this.target;

        const handlers = this.handlers.get(ev.type);
        let skip = -1;
        eventLoop: for (let i = 0; i < handlers.length; i++) {
            let prevReturns = this.returns;

            if (i === skip) {
                skip = -1;
                continue;
            }
            if (i > skip && skip !== -1) {
                skip = -1;
            }

            const fn = handlers[i];
            try {
                this.returns = await fn(ev);
                if (this.returns === false) break;
                switch (this.returns) {
                    case UserButtonEventHandler.bTrue:
                        skip = i + 2;
                        this.returns = true;
                        break;
                    case UserButtonEventHandler.bFalse:
                        skip = i + 1;
                        this.returns = false;
                        break;
                    case UserButtonEventHandler.bBreak:
                        break eventLoop;
                    case UserButtonEventHandler.bContinue:
                        this.returns = prevReturns;
                        continue eventLoop;
                    default:
                        break;
                }
            } catch (e) {
                console.error(e);
                await AlertDialog.alert('Error occurred on invoke custom button events', e.message);
                break;
            }
        }

        if (ev.type !== 'create') {
            for (const [key, value] of this.variables) {
                if (cachedVariable.has(key)) {
                    cachedVariable.set(key, value);
                }
            }
            this.variables = cachedVariable;
            this.constants = cachedConstant;
        }
    }
    static isValidIdentifier(name) {
        return /^[A-Za-z_][\w-]*$/.test(name);
    }
    addVariable(name, value) {
        if (!UserButtonEventHandler.isValidIdentifier(name)) {
            throw new TypeError(`${name} is not a valid identifier`);
        }
        if (this.constants.has(name)) {
            throw new TypeError(`${name} is already defined as constant`);
        }
        this.variables.set(name, value);
    }
    addConstant(name, value) {
        if (!UserButtonEventHandler.isValidIdentifier(name)) {
            throw new TypeError(`${name} is not a valid identifier`);
        }
        if (this.constants.has(name) || this.variables.has(name)) {
            throw new TypeError(`${name} is already defined`);
        }
        this.constants.set(name, value);
    }
    getResource(name, lang) {
        if (name === null || name === undefined || name === '') {
            throw new TypeError('resource name cannot be empty');
        }
        if (this.buttonResource === null || this.buttonResource === undefined) {
            return '';
        }
        try {
            if (lang === 'auto') {
                lang = undefined;
            }
            const res = this.buttonResource[name] ?? '';
            if (typeof res === 'string') {
                return res;
            }
            return Utils.getI18NRes(this.buttonResource[name], lang === '' ? undefined : lang);
        } catch {
            return '';
        }
    }
    isEmpty(value) {
        return value === null || value === undefined || value === '';
    }
}

/**
 * 
 * @param {string} command 
 * @returns {SplitResult[]}
 */
const splitCommandArgs = (command) => {
    let escaped = false;
    let result = [];
    let cur = new SplitResult();
    let quot = 0;
    let allowEmpty = false;

    const pushAndUpdate = () => {
        if (cur.val !== '' || allowEmpty) {
            result.push(cur);
            cur = new SplitResult();
            allowEmpty = false;
        }
    }
    for (const c of command) {
        if (escaped) {
            cur.val += c;
            escaped = false;
            continue;
        }
        switch (c) {
            case '\'':
            case '"':
                const quotType = c === '\'' ? 1 : 2;
                if (quot === quotType) {
                    pushAndUpdate();
                    quot = 0;
                } else if (quot === 0) {
                    quot = quotType;
                    allowEmpty = true;
                } else {
                    break;
                }
                continue;
            case '\\':
                if (quot === 1) { break; }
                escaped = true;
                continue;
            case '{':
                if (quot !== 1) {
                    cur.markToken();
                }
                break;
            case '}':
                if (quot !== 1) {
                    cur.closeToken();
                }
                break;
            case ' ':
                if (quot == 0) {
                    pushAndUpdate();
                    continue;
                }
                break;
        }
        cur.val += c;
    }
    if (cur.val !== '' || allowEmpty)
        result.push(cur);
    return result;
}

const splitCommandLines = (commands) => {
    let escaped = false;
    let result = [];
    let cur = '';
    let quot = 0;

    for (const c of commands) {
        if (escaped) {
            if (c !== '\n') {
                cur += '\\' + c;
            }
            escaped = false;
            continue;
        }
        switch (c) {
            case '\'':
            case '"':
                cur += c;
                const quotType = c === '\'' ? 1 : 2;
                if (quot === quotType) {
                    quot = 0;
                } else if (quot == 0) {
                    quot = quotType;
                } else {
                    break;
                }
                continue;
            case '\\':
                if (quot === 1) {
                    break;
                }
                escaped = true;
                continue;
            case '\n':
                if (quot == 0) {
                    if (cur !== '') {
                        result.push(cur);
                        cur = '';
                    }
                    continue;
                }
                break;
        }
        cur += c;
    }
    if (cur !== '')
        result.push(cur);
    return result;
}

class UserButtonExecuter {
    static comment(args, handler) {
        return async (e) => {
            const argv = processCommandArg(args, handler);
            const msg = argv[0] ?? handler.target.getAttribute('data');
            if (msg === '' || msg === undefined || msg === null) {
                throw new TypeError('invalid arguments, message cannot be empty.')
            }
            if (await AlertDialog.confirm('', Utils.getI18NRes({
                default: '@en',
                'zh-CN': `是否允许 "${handler.target.textContent}" 发送如下评论?\n\n${argv[0] ?? handler.target.getAttribute('data')}`,
                'en': `Allow "${handler.target.textContent}" to send following comment?\n\n${argv[0] ?? handler.target.getAttribute('data')}`
            }), Utils.getI18NRes(PERMISSION_ALLOW_TEXT_RESOURCE), Utils.getI18NRes(PERMISSION_DENY_TEXT_RESOURCE))) {
                helperUtils.triggerButtonComment(e, msg);
            }
        }
    }
    static open(args, handler) {
        return (e) => {
            helperUtils.openInNewTab(e, ...processCommandArg(args, handler));
        }
    }
    static jump(args, handler) {
        return (e) => {
            helperUtils.jumpTo(e, ...processCommandArg(args, handler));
        };
    }
    static refresh(args, handler) {
        return () => {
            location.reload();
        };
    }
    static css(args, handler) {
        return (e) => {
            const [name, value] = processCommandArg(args, handler);
            if (name === undefined || name === '') {
                throw TypeError('invalid arguments, "css" needs at least 1 argument');
            }
            if (args[1] === undefined) {
                return handler.target.style.getPropertyValue(name);
            }
            handler.target.style.setProperty(name, value);
            return true;
        };
    }
    static alert(args, handler) {
        return async () => {
            return await AlertDialog.alert(...processCommandArg(args, handler));
        };
    }
    static confirm(args, handler) {
        return async () => {
            return await AlertDialog.confirm(...processCommandArg(args, handler));
        };
    }
    static prompt(args, handler) {
        return async () => {
            return await AlertDialog.prompt(...processCommandArg(args, handler)) ?? false;
        };
    }
    static #parseVarArgs(args, handler) {
        const results = [];
        if (args[0].val === 'number' || args[0].val === 'string') {
            results.push(args[0].val);
            results.push(args[1].val);
            results.push(args.length > 2 ? processCommandArg(args[2], handler) : undefined); // 2: value
        } else {
            results.push('string');
            results.push(args[results.length]);
            results.push(args[0].val);
            results.push(args.length > 1 ? processCommandArg(args[1], handler) : undefined);
        }
        return results;
    }
    static ['const'](args, handler) {
        () => {
            if (args.length < 2) {
                throw TypeError('invalid arguments, "const" needs at least 2 arguments');
            }
            const [type, name, value] = UserButtonExecuter.#parseVarArgs(args, handler);
            if (value === undefined) {
                throw TypeError('invalid arguments, "const" needs a value argument');
            }
            handler.addConstant(name, type === 'number' ? Number(value) : value);
        }
    }
    static ['let'](args, handler) {
        return () => {
            if (args.length < 2) {
                throw TypeError('invalid arguments, "let" needs at least 2 arguments');
            }
            const [type, name, value] = UserButtonExecuter.#parseVarArgs(args, handler);
            if (value === undefined) {
                throw TypeError('invalid arguments, "let" needs a value argument');
            }
            handler.addVariable(name, type === 'number' ? Number(value) : value);
        }
    }
    static resource(args, handler) {
        return () => {
            return handler.getResource(...processCommandArg(args, handler));
        };
    }
    static log(args, handler) {
        return () => {
            const message = processCommandArg(args, handler);
            switch (args[0].val) {
                case 'debug':
                case 'info':
                case 'warn':
                case 'error':
                    const logger = Reflect.get(console, args[0].val);
                    message.shift();
                    Reflect.apply(logger, console, message);
                    break;
                default:
                    console.log(...message);
                    break;
            }
        };
    }
    static check(args, handler) {
        return async () => {
            const argv = processCommandArg(args, handler);
            const checked = argv.length > 0 ? argv[0] : handler.returns;
            const result = !handler.isEmpty(checked);
            if (!result) {
                const viewRoot = document.createElement('div');
                viewRoot.style.maxWidth = '80em';
                viewRoot.style.maxHeight = '50em';
                viewRoot.style.overflow = 'auto';
                viewRoot.innerHTML = `<pre>Check failed at line: ${line + 1}.\n` +
                    `Script requires non-empty value, but got empty value: ${checked === '' ? '""' : checked}.</pre>`;
                const pre = document.createElement('pre');
                pre.style.fontFamily = `'ui-monospace','SFMono-Regular','SF Mono','DejaVu Sans Mono','Menlo',` +
                    `'Consolas','Liberation Mono','Courier New','monospace'`;
                pre.style.fontSize = '0.9em';
                viewRoot.append(pre);

                let scripts = "";
                const lineIdLength = String(line + 3).length;
                const prefix = ' '.repeat(lineIdLength);
                let start = Math.max(0, line - 2);
                if (start === 1) {
                    start = 0;
                }
                if (start > 0) {
                    scripts += `    ...\n`;
                }
                for (let idx = start; idx < Math.min(actionArr.length, line + 2); idx++) {
                    const id = `${[prefix]}${idx + 1}`;
                    scripts += `${idx === line ? '>>> ' : '    '}${id.substring(id.length - lineIdLength)}: ${actionArr[idx].replace(/\n/g, '\\n')}\n`;
                }
                pre.textContent = scripts;

                await AlertDialog.alert('Check failed', viewRoot);
            }
            return result;
        }
    }
    static remove(args, handler) {
        return (e) => { handler.target.remove(); };
    }
    static text(args, handler) {
        return (e) => {
            if (args.length < 1) {
                return handler.target.textContent;
            }
            const [text] = processCommandArg(args, handler);
            handler.target.textContent = text;
        };
    }
    static ['class'](args, handler) {
        return (e) => {
            const [action, className] = processCommandArg(args, handler);
            switch (action) {
                case 'add':
                    handler.target.classList.add(className);
                    break;
                case 'remove':
                    handler.target.classList.remove(className);
                    break;
                case 'toggle':
                    handler.target.classList.toggle(className);
                    break;
                case 'contains':
                    return handler.target.classList.contains(className);
                case undefined:
                    return handler.target.className;
                default:
                    throw TypeError(`invalid class action "${action}"`);
            };
        }
    }
    static #parseIncDecArgs(args, handler) {
        const name = args[0].val;
        const stepStr = args.length > 1 ? processCommandArg(args[1], handler) : 1;
        const step = Number(stepStr);
        if (isNaN(step)) {
            throw TypeError(`invalid step value "${stepStr}"`);
        }
        const curVal = handler.variables.get(name);
        if (curVal === undefined) {
            throw TypeError(`variable "${name}" is not defined`);
        }
        if (typeof curVal !== 'number') {
            throw TypeError(`variable "${name}" is not a number`);
        }
        return [name, step, curVal];
    }
    static increase(args, handler) {
        return (e) => {
            if (args.length < 1) {
                throw TypeError('invalid arguments, "increase" needs at least 1 argument');
            }
            const [name, step, curVal] = UserButtonExecuter.#parseIncDecArgs(args, handler);
            handler.addVariable(name, curVal + step);
            return curVal + step;
        }
    }
    static decrease(args, handler) {
        return (e) => {
            if (args.length < 1) {
                throw TypeError('invalid arguments, "decrease" needs at least 1 argument');
            }
            const [name, step, curVal] = UserButtonExecuter.#parseIncDecArgs(args, handler);
            handler.addVariable(name, curVal - step);
            return curVal - step;
        }
    }
    /**
     * 
     * @param {SplitResult[]} args 
     * @param {UserButtonEventHandler} handler 
     * @returns 
     */
    static ['if'](args, handler) {
        return (e) => {
            let check = handler.returns;
            if (args.length > 0) {
                check = processCommandArg(args[0], handler);
            }
            if (handler.isEmpty(check) || check === 'false' || check === 0 || check === false) {
                return UserButtonEventHandler.bFalse;
            }
            return UserButtonEventHandler.bTrue;
        }
    }
    static ['break'](args, handler) {
        return () => {
            return UserButtonEventHandler.bBreak;
        }
    }
    static ['continue'](args, handler) {
        return () => {
            return UserButtonEventHandler.bContinue;
        }
    }
    static get(args, handler) {
        return () => {
            if (args.length < 1) {
                throw TypeError('invalid arguments, "get" needs at least 1 argument');
            }
            const name = processCommandArg(args[0], handler);
            if (handler.variables.has(name)) {
                return handler.variables.get(name);
            }
            if (handler.constants.has(name)) {
                return handler.constants.get(name);
            }
            throw TypeError(`variable or constant "${name}" is not defined`);
        }
    }
    /**
     * 
     * @param {SplitResult[]} args 
     * @param {UserButtonEventHandler} handler 
     * @returns 
     */
    static call(args, handler) {
        const func = processCommandArg(args[0], handler);
        if (!handler.handlers.has(func)) {
            throw new TypeError(`function "${func}" is not defined.`);
        }
        if (func === 'create') {
            throw new TypeError('function "create" is not able to call.');
        }
        handler.handleEvent(new Event(func));
    }
    /**
     * 
     * @param {SplitResult[]} args 
     * @param {UserButtonEventHandler} handler 
     * @param {(fn:Function, timeout:number,...args: any[])=>number} timer 
     * @param {string} name 
     * @returns {number}
     */
    static #timeout(args, handler, timer, name) {
        return (e) => {
            const [timeout, command] = processCommandArg(args, handler);
            if (command === undefined) {
                throw new TypeError('invalid arguments, "timeout" needs at least 2 arguments')
            }
            const [, , ...argv] = args;
            const fn = Reflect.get(UserButtonExecuter, command);
            if (fn === undefined) {
                throw new TypeError(`invalid command ${command}`);
            }
            const constants = handler.constants;
            const variables = handler.variables;
            const id = timer((argv) => {
                const cachedConstants = handler.constants;
                const cachedVariables = handler.variables;
                handler.constants = constants;
                handler.variables = variables;
                Reflect.apply(fn, UserButtonExecuter, [argv, handler])(new Event(name));
                handler.constants = cachedConstants;
                handler.variables = cachedVariables;
            }, Number(timeout), argv);
            handler.timers.add(id);
            return id;
        };
    }
    static timeout(args, handler) {
        return UserButtonExecuter.#timeout(args, handler, setTimeout, 'timeout');
    }
    static interval(args, handler) {
        return UserButtonExecuter.#timeout(args, handler, setInterval, 'interval');
    }
    /**
     * 
     * @param {SplitResult[]} args 
     * @param {UserButtonEventHandler} handler 
     * @returns {void}
     */
    static cleartimeout(args, handler) {
        return (e) => {
            const id = Number(processCommandArg(args[0], handler));
            if (handler.timers.has(id)) {
                clearTimeout(id);
                handler.timers.delete(id);
            }
        }
    }
    /**
     * 
     * @param {SplitResult[]} args 
     * @param {UserButtonEventHandler} handler 
     * @returns {void}
     */
    static clearinterval(args, handler) {
        return (e) => {
            const id = Number(processCommandArg(args[0], handler));
            if (handler.timers.has(id)) {
                clearTimeout(id);
                handler.timers.delete(id);
            }
        }
    }
}

/**
 * @param {UserButtonEventHandler} handler
 * @param {string} event 
 * @param {string} action 
 */
const userButtonAddEvents = (handler, event, actions) => {
    const actionArr = [];
    if (actions instanceof Array) {
        actions.forEach(cmd => {
            actionArr.push(...splitCommandLines(cmd));
        });
    } else {
        actionArr.push(...splitCommandLines(actions));
    }
    const events = handler.handlers.get(event) ?? [];
    handler.handlers.set(event, events);
    actionArr.forEach((action, line) => {
        const [option, ...args] = splitCommandArgs(action);
        const fn = Reflect.getOwnPropertyDescriptor(UserButtonExecuter, option.val);
        if (fn === undefined || fn.value === undefined) {
            throw TypeError(`invalid button action "${option.val}"`);
        }
        events.push(fn.value(args, handler));
    });
}

const g_existsButtonId = new Map();
const genHelperButton = (btnInfo) => {
    const label = Utils.getI18NRes(btnInfo.label);
    const { name, data, style: styles, events, comment } = btnInfo;
    if (name === undefined || name === '' || g_existsButtonId.has(name)) {
        throw TypeError('invalid button name');
    }
    const btn = document.createElement('button');
    btn.id = name;
    btn.classList.add('ci-helper-shortcut');
    btn.textContent = label;

    btn.setAttribute('title', Utils.getI18NRes(comment ?? ''));
    if (data) btn.setAttribute('data', data);
    if (styles) setButtonStyle(btn, styles);

    if (events) {
        for (let event in events) {
            if (event === 'create') {
                events[event].call(btnInfo, btn);
            } else {
                btn.addEventListener(event, events[event].bind(btnInfo));
            }
        }
    }
    g_existsButtonId.set(name, btn);
    return btn;
}

const genUserButton = (btnInfo) => {
    const label = Utils.getI18NRes(btnInfo.label);
    const { name, data, style: styles, events, comment } = btnInfo;
    if (name === undefined || name === '') {
        alert("button name cannot be empty");
        throw TypeError('invalid button name');
    }
    if (g_existsButtonId.has(name)) {
        alert("button name is already used");
        throw TypeError('Duplicate button name');
    }

    const btn = document.createElement('button');
    btn.setAttribute('title', Utils.getI18NRes(comment ?? ''));
    btn.id = name;
    btn.classList.add('ci-helper-shortcut');
    btn.textContent = label;
    if (data) btn.setAttribute('data', data);

    if (styles) {
        setButtonStyle(btn, styles);
    }

    if (events) {
        const handler = new UserButtonEventHandler(data, label, btnInfo.resources);
        handler.target = btn;
        for (let event in events) {
            userButtonAddEvents(handler, event, events[event])
            btn.addEventListener(event, handler);
        }
        btn.dispatchEvent(new Event('create'));
    }
    g_existsButtonId.set(name, btn);
    return btn;
}

/**
 * 
 * @param {ShadowRoot} btn 
 * @param {HTMLButtonElement} btn 
 * @param {string} position 
 */
const insertUserButton = (shadow, btn, position) => {
    const [method, relative] = position.split(':');
    const to = g_existsButtonId.get(relative);
    switch (method) {
        case 'begin':
            shadow.firstElementChild.before(btn);
            break;
        case 'end':
            shadow.append(btn);
            break;
        case 'before': {
            if (to === null) {
                throw new Error(`${relative} not existed`)
            }
            to.before(btn);
        }
            break;
        case 'after':
            if (to === null) {
                throw new Error(`${relative} not existed`);
            }
            to.after(btn);
            break;
        default:
            throw new TypeError('invalid position rule');
    }
}
